iT邦幫忙

2023 iThome 鐵人賽

DAY 25
0
2. 超級使用者可建立超級使用者

小獅:這容易,改個參數而已

老獅:別忘記要測試最後使用者在資料庫的權限是否真的是如同你輸入的一樣

# src/tests/test_services/test_users.py
async def test_superuser_can_create_superuser(
    db: sqlalchemy_asyncio.AsyncSession,
    client: httpx.AsyncClient,
    superuser_client: httpx.AsyncClient,
):
    username = str(uuid.uuid4())
    password = str(uuid.uuid4())
    user_info = {
        "username": username,
        "password": password,
        "is_superuser": True,
    }
    resp = await superuser_client.post("/v1/auth/users", json=user_info)
    assert resp.status_code == 201
    resp = await client.post(
        "/v1/auth/users/tokens",
        json=user_info,
    )
    assert resp.status_code == 200
    user = await auth_crud.user.get_by_username(db, username)
    assert user.is_superuser is user_info["is_superuser"]

老獅:有沒有發現他和上面那個 test_superuser_can_create_user 有 87% 相似

小獅:有喔,有什麼好方法讓他看起來比較不會那麼重複嗎?

老獅:pytest 提供了不少傳入測試參數的方法,我們可以簡單地給予 @pytest.mark.parametrize 這樣的 decorator 讓他自動產生不同參數,並且餵到該測試的參數中,他也會幫你跑成多個測試,讓你可以方便知道,哪個參數的測試壞了

# src/tests/test_services/test_users.py

@pytest.mark.parametrize("is_superuser", [True, False])
async def test_superuser_can_create_user(
    db: sqlalchemy_asyncio.AsyncSession,
    client: httpx.AsyncClient,
    superuser_client: httpx.AsyncClient,
    is_superuser: bool,
):
    username = str(uuid.uuid4())
    password = str(uuid.uuid4())
    user_info = {
        "username": username,
        "password": password,
        "is_superuser": is_superuser,
    }
    resp = await superuser_client.post("/v1/auth/users", json=user_info)
    assert resp.status_code == 201
    resp = await client.post(
        "/v1/auth/users/tokens",
        json=user_info,
    )
    assert resp.status_code == 200
    user = await auth_crud.user.get_by_username(db, username)
    assert user.is_superuser is user_info["is_superuser"]
make test
pytest .
============================= test session starts ==============================
platform darwin -- Python 3.8.13, pytest-7.4.0, pluggy-1.2.0
rootdir: /Users/super/project/fastit
configfile: pyproject.toml
plugins: asyncio-0.21.1, anyio-3.7.1
asyncio: mode=auto
collecting ... /Users/super/project/fastit/src
collected 8 items

src/tests/test_main.py .                                                 [ 12%]
src/tests/test_services/test_hashes.py .                                 [ 25%]
src/tests/test_services/test_token.py ...                                [ 62%]
src/tests/test_services/test_users.py ..                                 [ 87%]
src/tests/test_units/test_users_crud.py .                                [100%]

============================== 8 passed in 3.52s ===============================

小獅:好,剩下權限的部分

3. 一般使用者不能建立帳號密碼
# src/tests/test_services/test_users.py
@pytest.fixture
async def user(migrate: None, db: sqlalchemy_asyncio.AsyncSession) -> auth_models.User:
    return await auth_crud.user.create(
        db,
        {
            "username": str(uuid.uuid4()),
            "password": str(uuid.uuid4()),
            "is_superuser": False,
        },
    )


@pytest.fixture
async def user_client(
    user: auth_models.User,
) -> typing.AsyncIterator[httpx.AsyncClient]:
    from app.api.v1.endpoints.auth.users import tokens

    async with httpx.AsyncClient(
        app=main.app,
        base_url="http://test",
    ) as async_client:
        token = tokens.create_access_token(dict(sub=user.username))
        async_client.headers = {"authorization": f"Bearer {token}"}
        yield async_client


async def test_user_cannot_create_superuser(
    db: sqlalchemy_asyncio.AsyncSession,
    user_client: httpx.AsyncClient,
):
    username = str(uuid.uuid4())
    password = str(uuid.uuid4())
    user_info = {
        "username": username,
        "password": password,
        "is_superuser": True,
    }
    resp = await user_client.post("/v1/auth/users", json=user_info)
    assert resp.status_code == 403
make test
# 省略
=========================== short test summary info ============================
FAILED src/tests/test_services/test_users.py::test_user_cannot_create_superuser - assert 201 == 403
========================= 1 failed, 8 passed in 3.90s ==========================
make: *** [test] Error 1

小獅:我們要想辦法從 DB 撈出使用者是不是 superuser

src/app/api/v1/endpoints/auth/users/users.py
@@ -1,17 +1,42 @@
 import fastapi
+import jose
+from fastapi import security
+from jose import jwt
 from sqlalchemy.ext import asyncio as sqlalchemy_asyncio

 from app.api import dependencies
 from app.crud import auth as auth_crud
 from app.schemas import users as users_schemas
+from core import config

 router = fastapi.APIRouter()


 @router.post("", status_code=201)
 async def create_user(
+    token: str = fastapi.Depends(security.OAuth2PasswordBearer(tokenUrl="token")),
     user_info: users_schemas.UserInfo = fastapi.Body(),
     db: sqlalchemy_asyncio.AsyncSession = fastapi.Depends(dependencies.get_db),
 ):
+    credentials_exception = fastapi.HTTPException(
+        status_code=fastapi.status.HTTP_401_UNAUTHORIZED,
+        detail="Could not validate credentials",
+        headers={"WWW-Authenticate": "Bearer"},
+    )
+    try:
+        payload = jwt.decode(
+            token,
+            config.Settings().authjwt_secret_key,
+            algorithms=["HS256"],
+        )
+        username: str = payload.get("sub")
+        if username is None:
+            raise credentials_exception
+    except jose.JWTError:
+        raise credentials_exception
+    action_user = await auth_crud.user.get_by_username(db, username)
+
+    if not action_user.is_superuser:
+        raise fastapi.HTTPException(status_code=fastapi.status.HTTP_403_FORBIDDEN)

老獅:要不要匿名使用者也測一下

小獅:好主意,原本的 client 就是匿名使用者了對吧!

老獅:是的,因為我們沒有幫他放 tokenheader

async def test_anonymous_cannot_create_superuser(
    db: sqlalchemy_asyncio.AsyncSession,
    client: httpx.AsyncClient,
):
    username = str(uuid.uuid4())
    password = str(uuid.uuid4())
    user_info = {
        "username": username,
        "password": password,
        "is_superuser": True,
    }
    resp = await client.post("/v1/auth/users", json=user_info)
    assert resp.status_code == 401
make test
pytest .
============================= test session starts ==============================
platform darwin -- Python 3.8.13, pytest-7.4.0, pluggy-1.2.0
rootdir: /Users/super/project/fastit
configfile: pyproject.toml
plugins: asyncio-0.21.1, anyio-3.7.1
asyncio: mode=auto
collecting ... /Users/super/project/fastit/src
collected 10 items

src/tests/test_main.py .                                                 [ 10%]
src/tests/test_services/test_hashes.py .                                 [ 20%]
src/tests/test_services/test_token.py ...                                [ 50%]
src/tests/test_services/test_users.py ....                               [ 90%]
src/tests/test_units/test_users_crud.py .                                [100%]

============================== 10 passed in 3.81s ==============================

小獅:又一堆重複的程式碼,不會又要重構吧

老獅:是的,相信我們可以做得更好,但是那是之後的事情,測試寫好以後,我們就不怕改壞了,先提交吧

git add src/app/api/v1/endpoints/auth/users/users.py
git add src/tests/test_services/test_users.py
git commit -m "feat: block none superuser permission client to create a user" -m "test: user cannot create superuser" -m "test: anonymous cannot create superuser"

本次目錄

.
├── Makefile
├── docker-compose.yml
├── pyproject.toml
├── requirements
│   ├── base.in
│   ├── base.txt
│   ├── development.in
│   └── development.txt
├── requirements.txt
├── setup.cfg
└── src
    ├── app
    │   ├── alembic.ini
    │   ├── api
    │   │   ├── dependencies.py
    │   │   └── v1
    │   │       ├── endpoints
    │   │       │   ├── __init__.py
    │   │       │   └── auth
    │   │       │       └── users
    │   │       │           ├── hashes.py
    │   │       │           ├── tokens.py
    │   │       │           └── users.py    # 修改
    │   │       └── routers.py
    │   ├── crud
    │   │   └── auth.py
    │   ├── db
    │   │   ├── __init__.py
    │   │   └── bases.py
    │   ├── main.py
    │   ├── migrations
    │   │   ├── README
    │   │   ├── env.py
    │   │   ├── script.py.mako
    │   │   └── versions
    │   │       ├── 0d59755649aa_add_is_superuser_flag.py
    │   │       └── b130fb2851db_add_user_table.py
    │   ├── models
    │   │   └── auth.py
    │   └── schemas
    │       ├── health_check.py
    │       ├── tokens.py
    │       └── users.py
    ├── core
    │   └── config.py
    └── tests
        ├── conftest.py
        ├── test_main.py
        ├── test_services
        │   ├── test_hashes.py
        │   ├── test_token.py
        │   └── test_users.py  # 修改
        └── test_units
            └── test_users_crud.py

上一篇
超級使用者 - 實作
下一篇
超級使用者 - 初始化 Script - 規劃
系列文
FastAPI 開發30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言